/* Copyright (c) 2012 swing-on-fire Team
 * 
 * This file is part of Swing-On-Fire (http://code.google.com/p/swing-on-fire), licensed under the terms of the MIT
 * License (MIT).
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
package com.google.code.swingonfire.animation;

import java.util.ArrayList;
import java.util.List;

/**
 * Base class for the animation framework
 * 
 * @author ham
 */
public class Animator implements Runnable
{

    private static final int MAX_FPS = 60;
    private static final double KEEP_ALIVE = 5;

    private static final Animator INSTANCE = new Animator();

    /**
     * Adds and executes the specified animation
     * 
     * @param animation the animation
     * @param consumer animation consumers
     */
    public static void start(Animation animation, AnimationConsumer... consumers)
    {
        INSTANCE.addAction(AnimatorAction.add(animation, consumers));
    }

    /**
     * Stops the specified animation.
     * 
     * @param animation the animation, may be null
     * @param target the target, may be null
     */
    public static <TYPE> void stop(Animation animation)
    {
        INSTANCE.addAction(AnimatorAction.remove(animation));
    }

    /**
     * Sets the error handler
     * 
     * @param errorHandler the error handler
     */
    public static void errorHandler(AnimationErrorHandler errorHandler)
    {
        INSTANCE.setErrorHandler(errorHandler);
    }

    private final List<AnimatorAction> actions;
    private final AnimationRunnable runnable;

    private AnimationErrorHandler errorHandler;
    private Thread thread = null;

    private Animator()
    {
        super();

        errorHandler = new DefaultAnimationErrorHandler();

        actions = new ArrayList<AnimatorAction>();
        runnable = new AnimationRunnable();

        runnable.setErrorHandler(errorHandler);
    }

    /**
     * Returns true if the thread of the animation framework is running
     * 
     * @return true if running
     */
    public boolean isRunning()
    {
        return thread != null;
    }

    /**
     * Ensures that the thread is running, starts it if necessary
     */
    public void ensureRunning()
    {
        synchronized (actions)
        {
            if (!isRunning())
            {
                thread = new Thread(this, "Swing-On-Fire Animation Thread");
                thread.setPriority(Thread.MIN_PRIORITY);
                thread.setDaemon(true);
                thread.start();
            }
        }
    }

    protected void addAction(AnimatorAction action)
    {
        synchronized (actions)
        {
            actions.add(action);
            actions.notify();
            ensureRunning();
        }
    }

    /**
     * @return the error handler
     */
    public AnimationErrorHandler getErrorHandler()
    {
        return errorHandler;
    }

    /**
     * @param errorHandler the error handler
     */
    public void setErrorHandler(AnimationErrorHandler errorHandler)
    {
        this.errorHandler = errorHandler;

        runnable.setErrorHandler(errorHandler);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void run()
    {
        System.out.println("Starting Animation Thread...");

        try
        {
            while (Thread.currentThread() == thread)
            {
                synchronized (actions)
                {
                    for (AnimatorAction action : actions)
                    {
                        action.perform(runnable);
                    }

                    actions.clear();
                }

                long nanoTime = System.nanoTime();
                double delay;

                try
                {
                    delay = runnable.execute(nanoTime / 1000000000d);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace(System.err);
                    break;
                }

                long wait;
                if ((Double.isNaN(delay)) || (Double.isInfinite(delay)) || (delay < 0))
                {
                    wait = (long) (KEEP_ALIVE * 1000000000);
                }
                else
                {
                    nanoTime = Math.max(nanoTime + (long) (delay * 1000000000), nanoTime + (1000000000 / MAX_FPS));
                    wait = Math.max(nanoTime - System.nanoTime(), 1);
                }

                synchronized (actions)
                {
                    if (actions.size() <= 0)
                    {
                        if (wait > (KEEP_ALIVE * 1000000000))
                        {
                            wait = (long) (KEEP_ALIVE * 1000000000);
                        }

                        try
                        {
                            actions.wait(wait / 1000000, (int) (wait % 1000000));
                        }
                        catch (InterruptedException e)
                        {
                            e.printStackTrace(System.err);
                            break;
                        }

                        if ((actions.isEmpty()) && (runnable.isAnimationsEmpty()))
                        {
                            break;
                        }
                    }
                }
            }
        }
        finally
        {
            thread = null;

            System.out.println("Animation Thread stopped.");
        }
    }

}
